package gameoflife.automaton;

import gameoflife.Heading;
import gameoflife.Pattern;
import gameoflife.Position;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class HashQueueAutomaton extends AbstractAutomaton {

	private static class Cell extends gameoflife.Cell {
		int peers = 0;
		boolean alive = true;

		Cell(Position position) {
			super(position);
		}
	}

	private final Map<Position, Cell> map = new HashMap<Position, Cell>();

	private int births = 0;
	private int deaths = 0;
	private int size = 0;

	private final ReadWriteLock lock = new ReentrantReadWriteLock();
	private final Lock mapRead = lock.readLock();
	private final Lock mapWrite = lock.writeLock();

	private final Queue<Cell> birthQueue = new ArrayDeque<Cell>();
	private final Queue<Cell> deathQueue = new ArrayDeque<Cell>();

	protected void step() {
		generation++;

		for (Cell cell : map.values()) {
			if (cell.alive) {
				if (!rule.survives(cell.peers)) {
					deathQueue.add(cell);
				}
				else {
					cell.age();
				}
			}
			else {
				if (rule.born(cell.peers)) {
					birthQueue.add(cell);
				}
			}
		}

		mapWrite.lock();
		births = birthQueue.size();
		deaths = deathQueue.size();
		size += births - deaths;

		final Position neighborPosition = new Position();
		while (!birthQueue.isEmpty()) {
			Cell cell = birthQueue.poll();
			cell.alive = true;
			cell.setAge(0);
			for (Heading heading : Heading.ALL) {
				heading.toPosition(cell, neighborPosition);

				Cell neighbor = map.get(neighborPosition);
				if (neighbor != null) {
					neighbor.peers++;
				}
				else {
					neighbor = new Cell(neighborPosition);
					neighbor.alive = false;
					neighbor.peers = 1;
					map.put(neighbor, neighbor);
				}
			}
		}
		while (!deathQueue.isEmpty()) {
			Cell cell = deathQueue.poll();
			if (cell.peers == 0) {
				map.remove(cell);
			}
			else {
				cell.alive = false;
			}
			for (Heading heading : Heading.ALL) {
				heading.toPosition(cell, neighborPosition);

				Cell neighbor = map.get(neighborPosition);
				if (neighbor != null) {
					neighbor.peers--;
					if (!neighbor.alive && neighbor.peers == 0) {
						map.remove(neighbor);
					}
				}
			}
		}
		mapWrite.unlock();
	}

	// iteration
	@Override
	public void accept(Visitor visitor) {
		mapRead.lock();
		try {
			for (Cell cell : map.values()) {
				if (cell.alive) {
					visitor.visit(cell);
				}
			}
		}
		finally {
			mapRead.unlock();
		}
	}

	private void add(Position position) {
		Cell cell = map.get(position);
		if (cell == null) {
			cell = new Cell(position);
			map.put(cell, cell);
			size++;
			Position neighborPosition = new Position();
			for (Heading heading : Heading.ALL) {
				heading.toPosition(cell, neighborPosition);

				Cell neighbor = map.get(neighborPosition);
				if (neighbor != null) {
					if (neighbor.alive) {
						cell.peers++;
					}
					neighbor.peers++;
				}
				else {
					neighbor = new Cell(neighborPosition);
					neighbor.alive = false;
					neighbor.peers++;
					map.put(neighbor, neighbor);
				}
			}
		}
		else if (!cell.alive) {
			cell.alive = true;
			size++;

			Position neighborPosition = new Position();
			for (Heading heading : Heading.ALL) {
				heading.toPosition(cell, neighborPosition);

				Cell neighbor = map.get(neighborPosition);
				if (neighbor != null) {
					neighbor.peers++;
				}
				else {
					neighbor = new Cell(neighborPosition);
					neighbor.alive = false;
					neighbor.peers++;
					map.put(neighbor, neighbor);
				}
			}			
		}
	}

	private void remove(Position position) {
		Cell cell = map.get(position);
		if (cell != null && cell.alive) {
			if (cell.peers == 0) {
				map.remove(position);
			}
			else {
				cell.alive = false;
			}
			size--;

			Position neighborPosition = new Position();
			for (Heading heading : Heading.ALL) {
				heading.toPosition(cell, neighborPosition);

				Cell neighbor = map.get(neighborPosition);
				if (neighbor != null) {
					neighbor.peers--;
					if (neighbor.peers == 0 && !neighbor.alive) {
						map.remove(neighbor);
					}
				}
			}
		}
	}

	// modifiers
	public synchronized void set(Position position, boolean value) {
		mapWrite.lock();
		if (value) {
			add(position);
		}
		else {
			remove(position);
		}
		mapWrite.unlock();

		stateChanged(0);
	}

	public synchronized void addAll(Collection<Position> positions) {
		mapWrite.lock();
		for (Position position : positions) {
			add(position);
		}
		mapWrite.unlock();

		stateChanged(0);
	}

	public synchronized void clear() {
		mapWrite.lock();
		map.clear();
		births = 0;
		deaths = 0;
		generation = 0;
		size = 0;
		mapWrite.unlock();

		stateChanged(0);
	}

	// getters
	public synchronized boolean isSet(Position position) {
		Cell cell = map.get(position);
		return cell != null && cell.alive;
	}

	public synchronized Cell getCell(Position position) {
		return map.get(position);
	}

	public synchronized int size() {
		return size;
	}

	public synchronized int births() {
		return births;
	}

	public synchronized int deaths() {
		return deaths;
	}

	@Override
	public synchronized Pattern toPattern() {
		final Collection<Position> positions = new ArrayList<Position>(map.values().size());
		accept(new Visitor() {
			@Override
			public void visit(gameoflife.Cell cell) {
				positions.add(cell);
			}
		});
		return new Pattern(getRule(), positions);
	}
}
